Prover Return Decoding Guide
This guide explains how to interpret and decode the returns from the CrossL2ProverV2 contract, using a practical multi-rollup example to demonstrate the complete process.
Overview
The setValueFromSource function demonstrates how to:
- Take a proof from the Prove API
- Validate cross-chain events using the prover contract
- Decode and process the returned data securely
This process involves several critical steps of data validation and decoding that ensure cross-chain security.
Understanding the Interface
CrossL2Prover validateEvent Function
function validateEvent(bytes calldata proof)
returns (
uint32 chainId, // Source chain identifier
address emittingContract, // Emitting contract address
bytes topics, // Concatenated Event topics
bytes unindexedData // Non-indexed event parameters
)
← Return Values
Four key pieces of validated data from the cross-chain event
🗄️ Data Format
Topics as concatenated bytes, unindexed data as ABI-encoded parameters
Raw Return Data Structure
[
11155420, // uint32: chainId (Optimism Sepolia)
"0x24B1D355f5B254aF86860bBe4214aEDe2DB1314E", // address: emittingContract
"0x...", // bytes: topic1 + topic2 + topic3
"0x..." // bytes: ABI encoded non-indexed parameters
]
Event Structure Reference
Understanding the origin event structure is crucial for proper decoding:
event ValueSet(
// → topic[0] = keccak(ValueSet(address,string,bytes,uint256,bytes32,uint256))
address indexed sender, // In topics[1]
string key, // In unindexedData
bytes value, // In unindexedData
uint256 nonce, // In unindexedData
bytes32 indexed hashedKey, // In topics[2]
uint256 version // In unindexedData
);
Topic Distribution: topics[0] contains the event signature hash, topics[1-2] contain indexed parameters, and non-indexed parameters are ABI-encoded in unindexedData.
Step-by-Step Decoding & Validation Process
1. Initial Proof Validation
First, validate the proof and extract the basic return values:
(
uint32 sourceChainId,
address sourceContract,
bytes memory topics,
bytes memory unindexedData
) = polymerProver.validateEvent(proof);
What you get:
sourceChainId: Origin chain identifiersourceContract: Contract that emitted the eventtopics: Concatenated event topics (requires parsing)unindexedData: ABI-encoded parameters (requires decoding)
2. Source Chain ID Validation
Validate that the event originated from an expected source chain:
require(allowedSourceChains[sourceChainId], "Invalid source chain");
Critical Security Check: This ensures the event came from an authorized chain. If skipped, an event emitted on a different chain can be used to spoof the intended chain.
3. Source Emitting Contract Validation
Validate that the event was emitted by the expected contract:
if(sourceContract != expectedSourceContract){
revert invalidEmittingAddress();
}
)c;
Critical Security Check: This ensures that the event originated from a trusted contract and not from an arbitrary contract on the source chain that can be deployed by anyone. If skipped, a malicious contract on an allowed source chain can spoof events with arbitrary data.
4. Topics Length Validation
The topics length must be checked against the expected length.
require(
topics_from_proof.length == EXPECTED_LENGTH,
"Topics length does not match expected length"
);
Critical Security Check: This validates that the topics length matches what is expected for the event being decoded. If this validation is skipped, arbitrary length topic arrays can be passed in and impact parsing of unindexed data.
5. Event Signature Validation
Parse the topics bytes into individual bytes32 values and verify the event signature:
// Parse topics into bytes32 array
bytes32[] memory topicsArray = new bytes32[](3);
require(topics.length == 96, "Invalid topics length"); // 3 * 32 bytes
assembly {
let topicsPtr := add(topics, 32) // Skip length prefix
for { let i := 0 } lt(i, 3) { i := add(i, 1) } {
mstore(
add(add(topicsArray, 32), mul(i, 32)),
mload(add(topicsPtr, mul(i, 32)))
)
}
}
// Verify event signature
bytes32 expectedSelector = keccak256("ValueSet(address,string,bytes,uint256,bytes32,uint256)");
require(topicsArray[0] == expectedSelector, "Invalid event signature");
Critical Security Check: This ensures only ValueSet events are processed and prevents attacks from similar events with different parameter types. If skipped, events with different parameter types incorrectly processed, leading to corrupted event data.
6. Topics Array — Indexed Data Extraction
Using the topicsArray parsed in the previous step, extract the indexed parameters:
address sender = address(uint160(uint256(topicsArray[1])));
bytes32 hashedKey = topicsArray[2];
Result:
topicsArray[0]: Event signature hash (validated in step 5)topicsArray[1]: Indexed sender address (padded to 32 bytes)topicsArray[2]: Indexed hashedKey
Conversion Logic:
sender:bytes32→uint256→uint160→addresshashedKey: Direct use (alreadybytes32)
Important: If skipped, indexed event parameters won't be available for processing.
7. Decode Non-Indexed Event Data
Extract the remaining parameters from the ABI-encoded data:
(
, // skip key (we use hashedKey from topics)
bytes memory value,
uint256 nonce,
uint256 version
) = abi.decode(
unindexedData,
(string, bytes, uint256, uint256)
);
Parameters Retrieved:
key: Skipped (usinghashedKeyfrom topics instead)value: The actual data to storenonce: For replay protectionversion: For version control
Important: If skipped, non-indexed event parameters won't be available for processing.
8. Implement Replay Protection at App Level
Prevent the same event from being processed multiple times:
// Create and verify unique proof hash for replay protection
bytes32 uniqueHash = keccak256(
abi.encodePacked(sourceChainId, sourceContract, hashedKey, nonce)
);
require(!usedUniqueHashes[uniqueHash], "hashKey already used");
usedUniqueHashes[uniqueHash] = true;
Critical Security Check: Applications should implement replay protection using data from their event directly to prevent duplicate processing of the same cross-chain event. If skipped, the same cross-chain event could be processed infinitely many times, leading to double-spending or duplicate state changes.
Common Pitfalls & Solutions
Critical Mistake: The validateEvent function returns event topics as a single bytes array, NOT a bytes32[] array.
❌ Incorrect Approach
( , , bytes memory topics, ) = crossL2Prover.validateEvent(proof);
bytes32 eventSig = bytes32(topics[0]); // ❌ Reads first BYTE, not first TOPIC
✅ Correct Implementation
// Example for an event with 3 topics
require(topics.length == 3 * 32, "Invalid topics length");
// 1. Create a memory array
bytes32[] memory topicsArray = new bytes32[](3);
// 2. Use assembly to parse the topics
assembly {
let topicsPtr := add(topics, 32) // Skip bytes length
mstore(add(topicsArray, 32), mload(topicsPtr))
mstore(add(topicsArray, 64), mload(add(topicsPtr, 32)))
mstore(add(topicsArray, 96), mload(add(topicsPtr, 64)))
}
// 3. ✅ Use the new array for your logic
bytes32 eventSig = topicsArray[0];
Rule of Thumb: Always parse the topics bytes variable into a bytes32[] array before use.
Production Security Checklist
Important: Proof for a given event is not unique and should not be used as a unique identifier for replay protection.
- Source Chain Validation
- Contract Validation
- Event Signature Validation
- Replay Protection
1. Source Chain Validation
Prevent event duplication from unauthorized chains:
mapping(uint32 => bool) public allowedSourceChains;
require(allowedSourceChains[sourceChainId], "Invalid source chain");
Purpose: Safeguards against event duplication from different chains.
2. Source Contract Validation
Ensure events only come from authorized contracts:
mapping(uint32 => mapping(address => bool)) public authorizedContracts;
require(
authorizedContracts[sourceChainId][sourceContract],
"Unauthorized source contract"
);
Purpose: Prevents event duplication from unauthorized contracts on allowed chains.
3. Event Signature Validation
Always verify the complete event signature:
bytes32 expectedSelector = keccak256("ValueSet(address,string,bytes,uint256,bytes32,uint256)");
require(topicsArray[0] == expectedSelector, "Invalid event signature");
// Even slight parameter changes generate different hashes
Critical: Validates both event name and exact parameter types/order.
Example of Different Signatures:
"ValueSet(string,address,bytes,uint256,bytes32,uint256)" → Different hash
"ValueSet(address,bytes,string,uint256,bytes32,uint256)" → Different hash
4. Replay Protection
Prevent duplicate processing of the same cross-chain event:
mapping(bytes32 => bool) public usedUniqueHashes;
bytes32 uniqueHash = keccak256(
abi.encodePacked(sourceChainId, sourceContract, hashedKey, nonce)
);
require(!usedUniqueHashes[uniqueHash], "hashKey already used");
usedUniqueHashes[uniqueHash] = true;
Purpose: Applications should implement replay protection using data from their event directly to ensure each cross-chain event is processed only once.
Complete Implementation Example
function setValueFromSource(bytes calldata proof) external {
// 1. Validate the proof
(
uint32 sourceChainId,
address sourceContract,
bytes memory topics,
bytes memory unindexedData
) = polymerProver.validateEvent(proof);
// 2. Source chain ID validation
require(allowedSourceChains[sourceChainId], "Invalid source chain");
// 3. Source emitting contract validation
require(authorizedContracts[sourceChainId][sourceContract], "Unauthorized contract");
// 4. Topics length validation
require(topics.length == 96, "Invalid topics length");
// 5. Event signature validation (includes topics parsing)
bytes32[] memory topicsArray = new bytes32[](3);
assembly {
let topicsPtr := add(topics, 32)
mstore(add(topicsArray, 32), mload(topicsPtr))
mstore(add(topicsArray, 64), mload(add(topicsPtr, 32)))
mstore(add(topicsArray, 96), mload(add(topicsPtr, 64)))
}
bytes32 expectedSelector = keccak256("ValueSet(address,string,bytes,uint256,bytes32,uint256)");
require(topicsArray[0] == expectedSelector, "Invalid event signature");
// 6. Extract indexed parameters from topics array
address sender = address(uint160(uint256(topicsArray[1])));
bytes32 hashedKey = topicsArray[2];
// 7. Decode non-indexed event data
(
, // skip key
bytes memory value,
uint256 nonce,
uint256 version
) = abi.decode(unindexedData, (string, bytes, uint256, uint256));
// 8. Implement replay protection at app level
bytes32 uniqueHash = keccak256(
abi.encodePacked(sourceChainId, sourceContract, hashedKey, nonce)
);
require(!usedUniqueHashes[uniqueHash], "hashKey already used");
usedUniqueHashes[uniqueHash] = true;
// Process the validated data
_processValidatedData(sender, hashedKey, value, nonce, version);
}
Security First: This example includes all critical security validations for production use, ensuring robust cross-chain event processing.